Skip to content

test(rest): type the unchecked route.handler call sites in rest.test.ts, and measure what the response literals actually are - #15582

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-14356-unchecked-response-literal-sites
Sep 5, 2026
Merged

test(rest): type the unchecked route.handler call sites in rest.test.ts, and measure what the response literals actually are#15582
os-litant merged 2 commits into
mainfrom
claude/issue-14356-unchecked-response-literal-sites

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #14356

Test-only. No published surface moves: packages/rest/tsconfig.json excludes **/*.test.ts from the build config, and this diff is one test file.

What this PR measures

packages/rest/src/rest.test.ts held five const res = { json: vi.fn(), ... } literals whose enclosing rest / route binding was declared any, so route.handler(req, res) checked neither argument. No argument-1 error was masking them, no as any sat on the response, and packages/rest/test-typecheck-debt.json — empty since #13454 — could never see them. They were green because nothing looked, not because they conformed.

Typing the enclosing binding is the whole change. The repairs below are what the resulting measurement then required.

Re-derived split, with the card's own controls (measured here, not inherited)

reading card / triage on 5c9e40a this branch, before the edit after
const res = { json: vi.fn() in src/rest.test.ts 26 26 24
res as any in the same file 21 21 21 — untouched
test-typecheck-debt.json entries {} {} {} — untouched
control: .handler( across src/*.test.ts 284 290 291

26 = 21 + 5 reproduces exactly. The control moving 284 to 290 across the same window confirms the instrument is live while this population is not.

The five were told apart from the twenty-one by line set, not by eye: the 26 literal line numbers are 1382 … 1762 (21 of them, each with a res as any on the following handler call) plus 1900, 1988, 3901, 3975, 4043, which have no cast anywhere. Located by content, since the card's 3900 / 3974 / 4042 had drifted: const route = rest.getRoutes().find( followed by route.handler(req, res), with an untyped enclosing binding. The other rest.getRoutes().find( sites at :696, :1880 and :4131 are not in this class.

The measurement

Typing the enclosing binding at all five and repairing argument 1 with the package's own httpRequestForRoute (#13377) — because tsc reports at most one argument-assignability error per call expression, so a request literal missing query / method / path simply masks argument 2 again — the test-layer program said, verbatim (the only edit is that the angle brackets of Mock<Procedure> are written as HTML entities, because GitHub's body sanitizer deletes generic-shaped tokens even inside a fence; every other byte is tsc's):

src/rest.test.ts(1902,31): error TS2345: Argument of type '{ json: Mock<Procedure>; status: Mock<Procedure>; }' is not assignable to parameter of type 'IHttpResponse'.
  Type '{ json: Mock<Procedure>; status: Mock<Procedure>; }' is missing the following properties from type 'IHttpResponse': send, header
src/rest.test.ts(1930,11): error TS18048: 'route' is possibly 'undefined'.
src/rest.test.ts(1931,7): error TS2345: Argument of type '{ params: { type: string; name: string; }; headers: { 'If-Match': string; }; body: {}; }' is not assignable to parameter of type 'IHttpRequest'.
  Type '{ params: { type: string; name: string; }; headers: { 'If-Match': string; }; body: {}; }' is missing the following properties from type 'IHttpRequest': query, method, path
src/rest.test.ts(1954,11): error TS18048: 'route' is possibly 'undefined'.
src/rest.test.ts(1955,7): error TS2345: Argument of type '{ params: { type: string; name: string; }; headers: {}; body: {}; }' is not assignable to parameter of type 'IHttpRequest'.
  Type '{ params: { type: string; name: string; }; headers: {}; body: {}; }' is missing the following properties from type 'IHttpRequest': query, method, path
src/rest.test.ts(1995,7): error TS2345: Argument of type '{ json: Mock<Procedure>; status: Mock<Procedure>; }' is not assignable to parameter of type 'IHttpResponse'.
  Type '{ json: Mock<Procedure>; status: Mock<Procedure>; }' is missing the following properties from type 'IHttpResponse': send, header

Three things in it, in order of how much they change the card:

1. The three sites the card predicted would conform, DO conform. Sites 3901 / 3975 / 4043 — the filterAppForUser gate, the dashboard-widget gate, and the readMeta helper of the default-config gate — appear nowhere above. Once argument 2 is genuinely checked, their four-member literals satisfy IHttpResponse. The card's "they would conform if anything looked" was an unmeasured claim; it is now a measurement, and it is true. The expected red did not arrive there.

2. The other two of the five are missing send and header. Sites 1900 and 1988 supply only json and status, and go red with exactly the TS2345 signature #13454 repaired and then deleted from this package's ledger. That is the answer to "what are the other two missing".

3. A red the card's own instrument could not have predicted — two further unchecked calls at 1930 / 1954. Both sit in the same describe block and reach the handler through the same getPutRoute(rest, path) helper, so typing that helper exposed them. They are not among the 26: their response is an anonymous literal written at the argument position, never bound to a const, so the census pattern const res = { json: vi.fn() cannot see them at all. Same class, same two missing members. The unchecked-call class in this file is seven, not five; the bound-literal census is a lower bound on it.

The repair

Every red above is repaired in the code. ⛔ Nothing was written into test-typecheck-debt.json: a shrink-only ledger is not where an error a fixture can simply stop having belongs, and an entry here would have re-created one layer up the "clean number that was never earned" this card is about. It stays {}, byte-identical.

  • 1900, 1988, 1930, 1954 adopt httpResponseTestDouble() (The 2 remaining ledgered TS2345 in packages/rest are hand-built IHttpResponse literals — they were masked by the request literals, not absent #13454), which computes its required-member set from the contract. The two ADR-0112 assertions at 1988 keep asserting code and status (expect(res.status).toHaveBeenCalledWith(409) plus the METADATA_CONFLICT code on res.json).
  • 3901, 3975, 4043 keep their literals exactly as they were. Swapping the builder in there would have destroyed the measurement this card exists to produce.
  • All seven take argument 1 from httpRequestForRoute(route!, ...), which derives method from the route and materializes path from the pattern, so the request cannot disagree with the route it is sent to.
  • ⛔ The typing was not weakened anywhere to make a red go away.

1930 / 1954 are a bounded in-place fix, named here rather than left to a reader to find. They are the same defect class as the card, their correct form is pinned by evidence already in the tree (src/http-response-test-builder.ts plus the two sibling sites #13454 already converted at 2063 / 2088), no other open PR head holds this file, and they add no gate family. They were also not optional: typing the shared getPutRoute helper is what makes 1900 and 1988 checkable at all, and that same edit reds them.

Explicitly not done

Changeset

None, and none is owed. pr-automation.yml, Check Changeset, WHICH LEVEL route 2: a PR that releases nothing — "tests-only, and the like" — takes the skip-changeset label, which is preferred over an empty changeset. This diff is one *.test.ts file, excluded from the package's build config and from the published artifact.

Verification

All of it on the merged head c4bde186f53, exit codes captured before any pipe.

  • pnpm --filter '@objectstack/rest^...' build — exit 0 (run first; a stale dist/*.d.ts lies in both directions).
  • pnpm --filter @objectstack/rest typecheck — exit 0. Its second half prints: check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 0 file(s) / 0 error(s) / 0 pinned signature(s) held in test-typecheck-debt.json.
  • pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 — exit 0, 179 files / 3056 tests passed. Adding method / path to the requests and send / header to four responses changed no assertion outcome.
  • pnpm lint — the FULL repo sweep, eslint . --no-inline-config, exit 0. No narrowing claimed.
  • Gate union re-derived from the real changed path by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack on this head, which does not call the tree stale. All green: check:cross-package-test-inputs, check:dispatcher-error-vocabulary, check:doc-authoring, check:logger-receiver-detach, check:objectql-double-limit, check:org-identifier, check:page-declaration-shape, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:where-matcher, check:nul-bytes, check:driver-memory-census, check-reference-carrier-shape (and self-test), check-ci-filter-parity, check-comment-mask-adoption (and self-test), check-keyed-text-bounds (and self-test), check-platform-object-tenancy-census, check-plugin-teardown-shape, check-registry-log-declared (and self-test), check-system-context-census, check-undeclared-dep-imports.
  • Artifact-roster families score silent for every card, so they were run rather than read as a clearance: check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:engine-double-contract, check:error-status-conformance, check:single-claim-paths — all exit 0.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

….test.ts

`rest.test.ts` held five `const res = { json: vi.fn(), ... }` literals whose
enclosing `rest` / `route` binding was declared `any`, so `route.handler(req,
res)` checked neither argument. No repair to argument 1 could ever surface
them and `test-typecheck-debt.json` could never see them: they were green
because nothing looked, not because they conformed.

Typing the enclosing binding at all five turns that into a measurement:

  - 3 of them (the `filterAppForUser` gate, the dashboard widget gate, and
    the `readMeta` helper of the default-config gate) already supplied all
    four members `IHttpResponse` requires. They CONFORM — tsc reports nothing
    at those three call sites once argument 2 is actually checked.
  - 2 of them supplied only `json` and `status`, and go red with exactly the
    TS2345 `missing the following properties from type 'IHttpResponse': send,
    header` that #13454 repaired and cleared from the ledger. Repaired the
    same way: `httpResponseTestDouble()`.

Typing the shared `getPutRoute(rest, path)` helper also exposed 2 further
unchecked calls in the same describe block, at 1930 / 1954, that the census
pattern `const res = { json: vi.fn()` cannot see at all: their response is an
anonymous literal written at the argument position rather than bound to a
`const`. Same class, same two missing members, same repair.

Argument 1 is repaired at all seven with the package's own
`httpRequestForRoute(route!, …)` (#13377), because tsc reports at most one
argument-assignability error per call expression — leaving a request literal
missing `query` / `method` / `path` would simply mask argument 2 again and
put the sites back where they started.

`test-typecheck-debt.json` is untouched and stays empty: the shrink-only
ledger is not the place for an error a fixture can just stop having.

⛔ Not touched, deliberately: the 21 `res as any` sites (#13377 declined that
census for the request half and left 151 sites on purpose), and the mirrored
`makeRes()` fixtures — the three named files plus the in-file `invoke()`
helper at 4141 — where a mirror keeps only the LAST status while `mock.calls`
keeps every one, so substitution would change the assertion rather than the
fixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
@github-actions github-actions Bot added the size/s label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 954cb0bbfa02a5f19b8d82f64179a5e9a211bbe0packageMentionDocs.

@github-actions github-actions Bot added the tests label Sep 4, 2026
@os-litant os-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026 — with Claude
@os-litant
os-litant marked this pull request as ready for review September 4, 2026 23:33
@os-litant
os-litant enabled auto-merge September 4, 2026 23:33
@os-litant
os-litant added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit ef45c2f Sep 5, 2026
39 checks passed
@os-litant
os-litant deleted the claude/issue-14356-unchecked-response-literal-sites branch September 5, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants